package com.kkwrite.regimen.consumer.controller.login;

import com.alibaba.fastjson.JSON;
import com.kkwrite.regimen.common.entity.dto.BaseDTO;
import com.kkwrite.regimen.common.entity.dto.OutDTO;
import com.kkwrite.regimen.common.entity.dto.login.UserLoginInDTO;
import com.kkwrite.regimen.common.entity.dto.user.RegUserDTO;
import com.kkwrite.regimen.common.util.MD5Util;
import com.kkwrite.regimen.consumer.common.WXApi;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import springfox.documentation.annotations.ApiIgnore;

import javax.annotation.Resource;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static com.kkwrite.regimen.common.constant.Constant.BOOLEAN_STRING_FALSE;
import static com.kkwrite.regimen.common.constant.Constant.BOOLEAN_STRING_TRUE;
import static com.kkwrite.regimen.consumer.controller.constant.Constant.SERVICE_PREFIX;

/**
 * Created by lisha on 2018/7/10 20:38.
 *
 * @author lisha
 */
@Api(value = "登录相关模块")
@RestController
@RequestMapping(value = "/login")
public class LoginController {

    private final Logger logger = LoggerFactory.getLogger(LoginController.class);

    @Resource
    private RestTemplate rest;
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @PostMapping(value = "/gettoken")
    @HystrixCommand(fallbackMethod = "loginError")
    @ApiOperation(value = "获取token接口")
    @ApiImplicitParam(name = "inDTO", value = "用户登录入参，code不能为空", required = true,
            paramType = "body", dataType = "UserLoginInDTO")
    public OutDTO login(@RequestBody UserLoginInDTO inDTO) {
        // 入参校验
        if (inDTO.isInvalid()) {
            throw new RuntimeException("入参不合法");
        }
        // 调用WX接口获取openId和session_key
        Map result = callCheckService(inDTO.getCode());
        if (result == null || result.containsKey("errcode")) {
            throw new RuntimeException("code无效或网络服务异常！");
        }
        String openId = (String) result.get("openid");
        String sessionKey = (String) result.get("session_key");
        // 判断是否有未过期的token
        if (stringRedisTemplate.opsForHash().hasKey(openId, "token")) {
            long expire = stringRedisTemplate.getExpire(openId);
            if (expire > 5) {
                String token = (String) stringRedisTemplate.opsForHash().get(openId, "token");
                // 缓存新的sessionKey
                stringRedisTemplate.opsForHash().put(token, "sessionKey", sessionKey);
                // 延长14d
                stringRedisTemplate.expire(openId, 14, TimeUnit.DAYS);
                // 延长有效期15d
                stringRedisTemplate.expire(token, 15, TimeUnit.DAYS);
                return OutDTO.of(BOOLEAN_STRING_TRUE, "OK", "OK", token);
            }
        }
        // 保存user，返回userId，若该user已存在，查询userId并返回
        String userId = getUserId(openId);
        if (userId == null) {
            throw new RuntimeException("userId服务异常！");
        }
        // 生成token
        String token = generateToken(openId, sessionKey, userId);
        if (token == null) {
            throw new RuntimeException("token服务异常！");
        }
        redisCatchLoginStatus(userId, openId, sessionKey, token);
        return OutDTO.of(BOOLEAN_STRING_TRUE, "OK", "OK", token);
    }

    public OutDTO loginError(UserLoginInDTO inDTO, Throwable t) {
        return OutDTO.of(BOOLEAN_STRING_FALSE, "获取token服务异常！异常信息：" + t.getMessage(), null, inDTO);
    }

    @PostMapping(value = "/checkalive")
    @HystrixCommand(fallbackMethod = "checkTokenError")
    @ApiOperation(value = "校验token是否过期接口")
    @ApiIgnore
    public OutDTO checkToken(@RequestBody String jsonParam) {
        // 入参校验
        if (StringUtils.isBlank(jsonParam)) {
            throw new RuntimeException("入参不合法");
        }
        String token;
        try {
            Map param = JSON.parseObject(jsonParam, Map.class);
            token = (String) param.get("token");
        } catch (Exception e) {
            throw new RuntimeException("入参不合法");
        }
        long expire = stringRedisTemplate.getExpire(token);
        if (expire > 0) {
            return OutDTO.of(BOOLEAN_STRING_TRUE, "001", "不过期", "001");
        } else {
            return OutDTO.of(BOOLEAN_STRING_TRUE, "002", "过期", "002");
        }
    }

    public OutDTO checkTokenError(String jsonParam, Throwable t) {
        return OutDTO.of(BOOLEAN_STRING_FALSE, "校验token服务异常！异常信息：" + t.getMessage(), null, jsonParam);
    }

    /**
     * 调用WX接口获取openId和session_key
     *
     * @param code js_code
     * @return 无异常返回：map{openid: openId, session_key: session_key}，反之，返回null
     */
    private Map callCheckService(String code) {
        RestTemplate restTemplate = new RestTemplate();
        String url = WXApi.JSCODE_TO_SESSION;
        url = url.replace("#APPID#", WXApi.APP_ID);
        url = url.replace("#SECRET#", WXApi.APP_SECRET);
        url = url.replace("#JSCODE#", code);
        url = url.replace("#GRANT_TYPE#", "authorization_code");
        Map result = null;
        try {
            String resultString = restTemplate.getForObject(url, String.class);
            result = JSON.parseObject(resultString, Map.class);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        return result;
    }

    /**
     * redis 保存登录状态
     *
     * @param userId     userId
     * @param openId     openId
     * @param sessionKey session_key
     * @param token      token
     */
    private void redisCatchLoginStatus(String userId, String openId, String sessionKey, String token) {
        // openId - token 缓存14天
        stringRedisTemplate.opsForHash().put(openId, "token", token);
        stringRedisTemplate.expire(openId, 14, TimeUnit.DAYS);
        // token - values
        Map<String, String> map = new HashMap<>(9);
        map.put("openId", openId);
        map.put("sessionKey", sessionKey);
        map.put("userId", userId);
        map.put("timestamp", String.valueOf(System.currentTimeMillis()));
        stringRedisTemplate.opsForHash().putAll(token, map);
        // token 缓存15天
        stringRedisTemplate.expire(token, 15, TimeUnit.DAYS);
    }

    /**
     * 依据openId获取userId
     *
     * @param openId 入参openId
     * @return 服务调用异常及返回状态码不为0时该方法返回null，反之返回userId
     */
    private String getUserId(String openId) {
        RegUserDTO userDTO = new RegUserDTO();
        userDTO.setOpenId(openId);
        BaseDTO result;
        try {
            result = rest.postForObject(SERVICE_PREFIX + "/user/checkOrSave", userDTO, BaseDTO.class);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            return null;
        }
        if ("0".equals(result.getCode())) {
            return result.getMessage();
        }
        return null;
    }

    /**
     * 生成token
     *
     * @param openId     openId
     * @param sessionKey session_key
     * @param userId     userId
     * @return 未发生异常时返回token值，发生异常返回null
     */
    private String generateToken(String openId, String sessionKey, String userId) {
        try {
            return MD5Util.digest(openId + sessionKey + userId + System.currentTimeMillis());
        } catch (UnsupportedEncodingException | NoSuchAlgorithmException e) {
            logger.error(e.getMessage(), e);
        }
        return null;
    }

}
